/*
 *	This file is part of Transdroid <http://www.transdroid.org>
 *
 *	Transdroid is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *
 *	Transdroid is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with Transdroid.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package org.transdroid.daemon.adapters.vuze;

import android.util.Xml;

import net.iharder.Base64;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.util.TlsSniSocketFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * Implements an XML-RPC-like client that build and parses XML following
 * Vuze's XML over HTTP plug-in (which unfortunately is incompatible with
 * the default XML-RPC protocol).
 * <p>
 * The documentation can be found at http://azureus.sourceforge.net/plugin_details.php?plugin=xml_http_if&docu=1#1
 * and some stuff is at http://wiki.vuze.com/index.php/XML_over_HTTP
 * <p>
 * A lot of it is copied from the org.xmlrpc.android library's XMLRPCClient
 * as can be found at http://code.google.com/p/android-xmlrpc
 *
 * @author erickok
 */
public class VuzeXmlOverHttpClient {

    private final static String TAG_REQUEST = "REQUEST";
    private final static String TAG_OBJECT = "OBJECT";
    private final static String TAG_OBJECT_ID = "_object_id";
    private final static String TAG_METHOD = "METHOD";
    private final static String TAG_PARAMS = "PARAMS";
    private final static String TAG_ENTRY = "ENTRY";
    private final static String TAG_INDEX = "index";
    private final static String TAG_CONNECTION_ID = "CONNECTION_ID";
    private final static String TAG_REQUEST_ID = "REQUEST_ID";
    private final static String TAG_RESPONSE = "RESPONSE";
    private final static String TAG_ERROR = "ERROR";
    private final static String TAG_TORRENT = "torrent";
    private final static String TAG_STATS = "stats";
    private final static String TAG_ANNOUNCE = "announce_result";
    private final static String TAG_SCRAPE = "scrape_result";
    private final static String TAG_CACHED_PROPERTY_NAMES = "cached_property_names";

    private DefaultHttpClient client;
    private HttpPost postMethod;
    private Random random;
    private String username;
    private String password;

    /**
     * XMLRPCClient constructor. Creates new instance based on server URI
     *
     * @param settings The server connection settings
     * @param uri      The URI of the XML RPC to connect to
     * @throws DaemonException Thrown when settings are missing or conflicting
     */
    public VuzeXmlOverHttpClient(DaemonSettings settings, URI uri) throws DaemonException {
        postMethod = new HttpPost(uri);
        postMethod.addHeader("Content-Type", "text/xml");

        // WARNING
        // I had to disable "Expect: 100-Continue" header since I had
        // two second delay between sending http POST request and POST body
        HttpParams httpParams = postMethod.getParams();
        HttpProtocolParams.setUseExpectContinue(httpParams, false);

        HttpConnectionParams.setConnectionTimeout(httpParams, settings.getTimeoutInMilliseconds());
        HttpConnectionParams.setSoTimeout(httpParams, settings.getTimeoutInMilliseconds());

        SchemeRegistry registry = new SchemeRegistry();
        SocketFactory httpsSocketFactory;
        if (settings.getSslTrustKey() != null) {
            httpsSocketFactory = new TlsSniSocketFactory(settings.getSslTrustKey());
        } else if (settings.getSslTrustAll()) {
            httpsSocketFactory = new TlsSniSocketFactory(true);
        } else {
            httpsSocketFactory = new TlsSniSocketFactory();
        }
        registry.register(new Scheme("http", new PlainSocketFactory(), 80));
        registry.register(new Scheme("https", httpsSocketFactory, 443));

        client = new DefaultHttpClient(new ThreadSafeClientConnManager(httpParams, registry), httpParams);
        if (settings.shouldUseAuthentication()) {
            if (settings.getUsername() == null || settings.getPassword() == null) {
                throw new DaemonException(DaemonException.ExceptionType.AuthenticationFailure, "No username or password set, while authentication was enabled.");
            } else {
                username = settings.getUsername();
                password = settings.getPassword();
                client.getCredentialsProvider().setCredentials(
                        new AuthScope(postMethod.getURI().getHost(), postMethod.getURI().getPort(), AuthScope.ANY_REALM),
                        new UsernamePasswordCredentials(username, password));
            }
        }

        random = new Random();
        random.nextInt();
    }

    /**
     * Convenience constructor. Creates new instance based on server String address
     *
     * @param settings The server connection settings
     * @param url      The URL of the XML RPC to connect to
     * @throws DaemonException Thrown when settings are missing or conflicting
     */
    public VuzeXmlOverHttpClient(DaemonSettings settings, String url) throws DaemonException {
        this(settings, URI.create(url));
    }

    static Object deserialize(String rawText) {

        // Null?
        if (rawText == null || rawText.equals("null")) {
            return null;
        }

		/* For now cast all integers as Long; this prevents casting problems later on when
		 * we know it's a long but the value was small so it is casted to an Integer here
		// Integer?
		try {
			Integer integernum = Integer.parseInt(rawText);
			return integernum;
		} catch (NumberFormatException e) {
			// Just continue trying the next type
		}*/

        // Long?
        try {
            return Long.parseLong(rawText);
        } catch (NumberFormatException e) {
            // Just continue trying the next type
        }

        // Double?
        try {
            return Double.parseDouble(rawText);
        } catch (NumberFormatException e) {
            // Just continue trying the next type
        }

        // String otherwise
        return rawText;
    }

    protected Map<String, Object> callXMLRPC(Long object, String method, Object[] params, Long connectionID, boolean paramsAreVuzeObjects) throws DaemonException {
        try {

            // prepare POST body
            XmlSerializer serializer = Xml.newSerializer();
            StringWriter bodyWriter = new StringWriter();
            serializer.setOutput(bodyWriter);
            serializer.startDocument(null, null);
            serializer.startTag(null, TAG_REQUEST);

            // set object
            if (object != null) {
                serializer.startTag(null, TAG_OBJECT).startTag(null, TAG_OBJECT_ID)
                        .text(object.toString()).endTag(null, TAG_OBJECT_ID).endTag(null, TAG_OBJECT);
            }

            // set method
            serializer.startTag(null, TAG_METHOD).text(method).endTag(null, TAG_METHOD);
            if (params != null && params.length != 0) {
                // set method params
                serializer.startTag(null, TAG_PARAMS);
                Integer entryIndex = 0;
                for (Object param : params) {
                    serializer.startTag(null, TAG_ENTRY).attribute(null, TAG_INDEX, entryIndex.toString());
                    if (paramsAreVuzeObjects) {
                        serializer.startTag(null, TAG_OBJECT).startTag(null, TAG_OBJECT_ID);
                    }
                    serializer.text(serialize(param));
                    if (paramsAreVuzeObjects) {
                        serializer.endTag(null, TAG_OBJECT_ID).endTag(null, TAG_OBJECT);
                    }
                    serializer.endTag(null, TAG_ENTRY);
                    entryIndex++;
                }
                serializer.endTag(null, TAG_PARAMS);
            }

            // set connection id
            if (connectionID != null) {
                serializer.startTag(null, TAG_CONNECTION_ID).text(connectionID.toString()).endTag(null, TAG_CONNECTION_ID);
            }
            // set request id, which for this purpose is always a random number
            int randomRequestID = random.nextInt();
            serializer.startTag(null, TAG_REQUEST_ID).text(Integer.toString(randomRequestID)).endTag(null, TAG_REQUEST_ID);

            serializer.endTag(null, TAG_REQUEST);
            serializer.endDocument();

            // set POST body
            HttpEntity entity = new StringEntity(bodyWriter.toString());
            postMethod.setEntity(entity);

            // Force preemptive authentication
            // This makes sure there is an 'Authentication: ' header being send before trying and failing and retrying
            // by the basic authentication mechanism of DefaultHttpClient
            postMethod.addHeader("Authorization", "Basic " + Base64.encodeBytes((username + ":" + password).getBytes()));

            // execute HTTP POST request
            HttpResponse response = client.execute(postMethod);

            // check status code
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
                throw new DaemonException(ExceptionType.AuthenticationFailure, "HTTP " + HttpStatus.SC_UNAUTHORIZED + " response (so no user or password or incorrect ones)");
            } else if (statusCode != HttpStatus.SC_OK) {
                throw new DaemonException(ExceptionType.ConnectionError, "HTTP status code: " + statusCode + " != " + HttpStatus.SC_OK);
            }

            // parse response stuff
            //
            // setup pull parser
            XmlPullParser pullParser = XmlPullParserFactory.newInstance().newPullParser();
            entity = response.getEntity();
            //String temp = HttpHelper.ConvertStreamToString(entity.getContent());
            //Reader reader = new StringReader(temp);
            Reader reader = new InputStreamReader(entity.getContent());
            pullParser.setInput(reader);

            // lets start pulling...
            pullParser.nextTag();
            pullParser.require(XmlPullParser.START_TAG, null, TAG_RESPONSE);

            // build list of returned values
            int next = pullParser.nextTag(); // skip to first start tag in list
            String name = pullParser.getName(); // get name of the first start tag

            // Empty response?
            if (next == XmlPullParser.END_TAG && name.equals(TAG_RESPONSE)) {

                return null;

            } else if (name.equals(TAG_ERROR)) {

                // Error
                String errorText = pullParser.nextText(); // the value of the ERROR
                entity.consumeContent();
                throw new DaemonException(ExceptionType.ConnectionError, errorText);

            } else {

                // Consume a list of ENTRYs?
                if (name.equals(TAG_ENTRY)) {

                    Map<String, Object> entries = new HashMap<>();
                    for (int i = 0; name.equals(TAG_ENTRY); i++) {
                        entries.put(TAG_ENTRY + i, consumeEntry(pullParser));
                        name = pullParser.getName();
                    }
                    entity.consumeContent();
                    return entries;

                } else {

                    // Only a single object was returned, not an entry listing
                    return consumeObject(pullParser);
                }

            }

        } catch (IOException e) {
            throw new DaemonException(ExceptionType.ConnectionError, e.toString());
        } catch (XmlPullParserException e) {
            throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
        }
    }

    private Map<String, Object> consumeEntry(XmlPullParser pullParser) throws XmlPullParserException, IOException {

        int next = pullParser.nextTag();
        String name = pullParser.getName();

        // Consume the ENTRY objects
        Map<String, Object> returnValues = new HashMap<>();
        while (next == XmlPullParser.START_TAG) {

            if (name.equals(TAG_TORRENT) || name.equals(TAG_ANNOUNCE) || name.equals(TAG_SCRAPE) || name.equals(TAG_STATS)) {
                // One of the objects contained inside an entry
                pullParser.nextTag();
                returnValues.put(name, consumeObject(pullParser));
            } else {
                // An object text inside this entry (such as _connectionid)
                returnValues.put(name, deserialize(pullParser.nextText()));
            }
            next = pullParser.nextTag(); // skip to next start tag
            name = pullParser.getName(); // get name of the new start tag

        }

        // Consume ENTRY ending
        pullParser.nextTag();

        return returnValues;

    }

    private Map<String, Object> consumeObject(XmlPullParser pullParser) throws XmlPullParserException, IOException {

        int next = XmlPullParser.START_TAG;
        String name = pullParser.getName();

        // Consume bottom-level (contains no objects of its own) object
        Map<String, Object> returnValues = new HashMap<>();
        while (next == XmlPullParser.START_TAG && !(name.equals(TAG_CACHED_PROPERTY_NAMES))) {

            if (name.equals(TAG_TORRENT) || name.equals(TAG_ANNOUNCE) || name.equals(TAG_SCRAPE) || name.equals(TAG_STATS)) {
                // One of the objects contained inside an object
                pullParser.nextTag();
                returnValues.put(name, consumeObject(pullParser));
            } else {
                // An object text inside this entry (such as _connectionid)
                returnValues.put(name, deserialize(pullParser.nextText()));
            }
            next = pullParser.nextTag(); // skip to next start tag
            name = pullParser.getName(); // get name of the new start tag

        }

        return returnValues;

    }

    private String serialize(Object value) {
        return value.toString();
    }

}
